"""
皮肤权重工具 - 根据物体到顶点的距离自动分配蒙皮权重

功能：
    基于选定物体到网格顶点的距离，使用平滑步长函数计算蒙皮权重分布

作者: LiuJingYu
创建日期: 2025-02-22
最后修改: 2025-02-22 (优化版本)
"""
'''第一版'''
# import pymel.core as pm
# import maya.OpenMaya as om
#
#
# def dis_weight(mesh, objList, s):
#     sh = mesh.getShapes()
#     skin = []
#     # 遍历形状节点列表
#     for shape in sh:
#         # 获取每个形状节点的历史节点，筛选出蒙皮节点
#         skin.extend(shape.history(type='skinCluster'))
#
#     if len(skin) < 1: om.MGlobal.displayError("没有蒙皮节点")
#     if len(skin) > 1: om.MGlobal.displayWarning("蒙皮节点过多")
#     skin = skin[0]
#     vtx_pos_list = [pm.xform(x, q=True, t=True, ws=True) for x in mesh.vtx]
#
#     dis_node = pm.shadingNode('distanceBetween', asUtility=True)
#
#     for obj in objList:
#         dis_node.p1.set(pm.xform(obj, q=True, t=True, ws=True))
#
#         dis = [(dis_node.p2.set(x), dis_node.d.get())[1] for x in vtx_pos_list]
#
#         [pm.skinPercent(skin, vtx, tv=(obj, 1 - pm.mel.smoothstep(0, s, i))) for i, vtx in zip(dis, mesh.vtx) if i < s]
#
#     pm.delete(dis_node)
#     return 0
#
#
# if __name__ == "__main__":
#     mesh = pm.selected()[0]
#     objList = pm.selected()[1:]
#     s = 10
#     dis_weight(mesh, objList, s)
"""第二版"""

# 导入pymel.core库，用于与Maya进行交互和操作场景中的节点
import pymel.core as pm
# 导入maya.OpenMaya库，提供了Maya的底层API，用于错误信息显示等操作
import maya.OpenMaya as om
# 导入logging库，用于记录程序运行过程中的信息，方便调试和错误追踪
import logging
# 从typing模块导入List和Tuple类型，用于类型提示，提高代码的可读性和可维护性
from typing import List, Tuple, Optional
import math


# # 配置日志记录
# # 设置日志的基本配置，包括日志级别、日志格式、日志文件名和文件打开模式
# logging.basicConfig(
#     # 日志级别设置为INFO，意味着会记录所有INFO及以上级别的日志信息
#     level=logging.INFO,
#     # 日志的格式，包含时间、日志器名称、日志级别和具体的日志消息
#     format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
#     # 日志文件的名称，所有日志信息会被写入该文件
#     filename="dis_weight.log",
#     # 文件打开模式为写入模式，每次运行程序会覆盖之前的日志文件
#     filemode="w"
# )
# # 创建一个名为__name__的日志记录器，用于记录程序运行过程中的信息
# logger = logging.getLogger(__name__)
# 
# 
# def smoothstep(min_val: float, max_val: float, value: float) -> float:
#     """
#     平滑过渡函数，在[min_val, max_val]区间内生成平滑的0-1过渡值
# 
#     Args:
#         min_val: 最小值阈值
#         max_val: 最大值阈值
#         value: 输入值
# 
#     Returns:
#         标准化后的平滑过渡值
#     """
#     # 如果输入值小于等于最小值阈值，返回0.0
#     if value <= min_val:
#         return 0.0
#     # 如果输入值大于等于最大值阈值，返回1.0
#     if value >= max_val:
#         return 1.0
#     # 归一化输入值，将其映射到[0, 1]区间
#     t = (value - min_val) / (max_val - min_val)
#     # 计算三次插值，得到平滑过渡值
#     return t * t * (3.0 - 2.0 * t)
# 
# 
# def dis_weight(mesh: pm.nodetypes.Transform,
#                obj_list: List[pm.nodetypes.Transform],
#                radius: float) -> int:
#     """
#     基于距离的蒙皮权重分配工具
# 
#     通过计算目标物体到网格顶点的距离，使用平滑过渡函数分配蒙皮权重。
#     权重值在指定半径范围内从1（最近）到0（最远）渐变。
# 
#     Args:
#         mesh: 目标网格物体
#         obj_list: 影响权重分配的控制物体列表
#         radius: 影响半径（单位：场景单位）
# 
#     Returns:
#         0 表示成功，非零表示错误代码
# 
#     Raises:
#         TypeError: 输入参数类型错误时抛出
#         ValueError: 输入参数值错误时抛出
#     """
#     # 参数验证
#     # 检查mesh参数是否为Transform类型，如果不是则记录错误日志并显示错误信息，返回错误代码1
#     if not isinstance(mesh, pm.nodetypes.Transform):
#         logger.error("网格参数必须是Transform类型")
#         om.MGlobal.displayError("网格参数必须是Transform类型")
#         return 1
# 
#     # 检查obj_list中的每个元素是否为Transform类型，如果有不符合的则记录错误日志并显示错误信息，返回错误代码2
#     if not all(isinstance(obj, pm.nodetypes.Transform) for obj in obj_list):
#         logger.error("目标物体列表包含无效类型")
#         om.MGlobal.displayError("目标物体列表包含无效类型")
#         return 2
# 
#     # 检查半径是否大于0，如果不大于0则记录错误日志并显示错误信息，返回错误代码3
#     if radius <= 0:
#         logger.error("影响半径必须大于0")
#         om.MGlobal.displayError("影响半径必须大于0")
#         return 3
# 
#     try:
#         # 获取蒙皮簇
#         # 初始化一个空列表，用于存储找到的蒙皮簇节点
#         skin_clusters = []
#         # 遍历网格的所有形状节点
#         for shape in mesh.getShapes():
#             # 查找每个形状节点历史记录中的蒙皮簇节点，并添加到skin_clusters列表中
#             skin_clusters.extend(shape.history(type="skinCluster"))
# 
#         # 如果没有找到蒙皮簇节点，记录错误日志并显示错误信息，返回错误代码4
#         if not skin_clusters:
#             logger.error("未找到蒙皮簇")
#             om.MGlobal.displayError("未找到蒙皮簇")
#             return 4
# 
#         # 取第一个蒙皮簇节点作为当前使用的蒙皮簇
#         skin = skin_clusters[0]
#         # 如果找到多个蒙皮簇节点，记录警告日志并显示警告信息
#         if len(skin_clusters) > 1:
#             logger.warning("发现多个蒙皮簇，使用第一个")
#             om.MGlobal.displayWarning("发现多个蒙皮簇，使用第一个")
# 
#         # 验证控制物体是否为影响物体
#         # 获取蒙皮簇的所有影响物体列表
#         influences = skin.influenceObjects()
#         # 遍历控制物体列表
#         for obj in obj_list:
#             # 如果某个控制物体不在影响物体列表中，记录错误日志并显示错误信息，返回错误代码5
#             if obj not in influences:
#                 logger.error(f"物体 {obj} 不是蒙皮影响物体")
#                 om.MGlobal.displayError(f"物体 {obj} 不是蒙皮影响物体")
#                 return 5
# 
#         # 获取顶点位置数据
#         # 获取网格所有顶点的世界空间位置，并存储在列表中
#         vtx_positions = [pm.xform(vtx, q=True, t=True, ws=True)
#                          for vtx in mesh.vtx]
#         # 获取控制物体列表中所有物体的世界空间位置，并存储在列表中
#         obj_positions = [pm.xform(obj, q=True, t=True, ws=True)
#                          for obj in obj_list]
# 
#         # 遍历每个控制物体
#         for obj, obj_pos in zip(obj_list, obj_positions):
#             # 记录当前处理的控制物体信息
#             logger.info(f"处理控制物体: {obj}")
# 
#             # 遍历所有顶点
#             for vtx, vtx_pos in zip(mesh.vtx, vtx_positions):
#                 # 计算欧氏距离
#                 # 计算当前顶点到当前控制物体的欧氏距离
#                 distance = sum((a - b) ** 2 for a, b in zip(vtx_pos, obj_pos)) ** 0.5
# 
#                 # 如果距离小于半径
#                 if distance < radius:
#                     # 计算平滑权重
#                     # 使用smoothstep函数计算平滑权重，权重值在[0, 1]之间
#                     weight = 1 - smoothstep(0, radius, distance)
#                     # 设置蒙皮权重
#                     # 为当前顶点设置对当前控制物体的蒙皮权重，不进行归一化处理
#                     pm.skinPercent(
#                         skin,
#                         vtx,
#                         transformValue=[(obj, weight)],
#                         normalize=False
#                     )
# 
#         # 记录权重分配完成的信息
#         logger.info("权重分配完成")
#         # 在Maya中显示权重分配完成的信息
#         om.MGlobal.displayInfo("蒙皮权重分配完成")
#         # 返回成功代码0
#         return 0
# 
#     except Exception as e:
#         # 记录未预期错误的详细信息
#         logger.exception("处理过程中发生未预期错误")
#         # 在Maya中显示错误信息
#         om.MGlobal.displayError(f"错误发生: {str(e)}")
#         # 返回错误代码6
#         return 6
# 
# 
# if __name__ == "__main__":
#     try:
#         # 获取用户选择
#         # 获取用户在Maya场景中选择的所有物体
#         selection = pm.selected()
# 
#         # 验证选择
#         # 如果选择的物体数量小于2，记录错误日志并显示错误信息，弹出确认对话框提示用户选择数量不足，抛出ValueError异常
#         if len(selection) < 2:
#             logger.error("需要至少选择一个网格和一个控制物体")
#             om.MGlobal.displayError("需要至少选择一个网格和一个控制物体")
#             pm.confirmDialog(title="错误", message="选择数量不足")
#             raise ValueError("选择数量不足")
# 
#         # 交互式获取半径参数
#         # 弹出一个对话框，提示用户输入影响半径，用户可以选择OK或Cancel
#         radius = pm.promptDialog(
#             title="输入影响半径",
#             message="输入影响半径（场景单位）:",
#             button=["OK", "Cancel"],
#             defaultButton="OK",
#             cancelButton="Cancel",
#             dismissString="Cancel"
#         )
# 
#         # 如果用户选择了Cancel，记录信息并显示警告信息，抛出SystemExit异常退出程序
#         if radius == "Cancel":
#             logger.info("用户取消操作")
#             pm.warning("操作已取消")
#             raise SystemExit("用户取消操作")
# 
#         # 获取用户输入的半径值，并转换为浮点数
#         radius = float(pm.promptDialog(q=True, text=True))
# 
#         # 执行权重分配
#         # 调用dis_weight函数进行蒙皮权重分配，并获取返回结果
#         result = dis_weight(
#             mesh=selection[0],
#             obj_list=selection[1:],
#             radius=radius
#         )
# 
#         # 显示结果
#         # 如果结果为0，表示权重分配成功，弹出确认对话框提示用户操作完成
#         if result == 0:
#             pm.confirmDialog(
#                 title="完成",
#                 message="权重分配成功完成",
#                 button=["OK"]
#             )
#         # 否则，弹出确认对话框提示用户操作失败，并显示错误代码
#         else:
#             pm.confirmDialog(
#                 title="错误",
#                 message=f"操作失败，错误代码: {result}",
#                 button=["OK"]
#             )
# 
#     except Exception as e:
#         # 记录主流程错误的详细信息
#         logger.exception("主流程错误")
#         # 在Maya中显示致命错误信息
#         om.MGlobal.displayError(f"致命错误: {str(e)}")